Imports System.Runtime.InteropServices
Imports System.Text

Namespace Encryption

  ''' <summary>
  ''' Use local machine or user to encrypt and decrypt the data
  ''' </summary>
  ''' <remarks></remarks>
  Public Enum EncryptionStorage
    ''' <summary>
    ''' Uses machine key to encrypt and decrypt data.
    ''' </summary>
    ''' <remarks></remarks>
    Machine
    ''' <summary>
    ''' Uses user key to encrypt and decrypt data.
    ''' </summary>
    ''' <remarks></remarks>
    User
  End Enum

  ''' <summary>
  ''' Uses the Data Protection API (DPAPI) to encrypt and decrypt secrets based on the logged in user or local machine. 
  ''' </summary>
  ''' <remarks></remarks>
  Public NotInheritable Class Dpapi

    Private _entropyData As Byte()

    ''' <summary>
    ''' Constructor
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub New()
      Me.SetEntropy()
    End Sub

    ''' <summary>
    ''' Encrypts data using local machine key.
    ''' </summary>
    ''' <param name="value"><see cref="String">Value</see> to encrypt.</param>
    ''' <returns>Encrypted <see cref="String">value</see>.</returns>
    ''' <remarks></remarks>
    Public Function Encrypt(ByVal value As String) As String

      Return Me.Encrypt(value, EncryptionStorage.Machine)

    End Function

    ''' <summary>
    ''' Encrypts data.
    ''' </summary>
    ''' <param name="value"><see cref="String">Value</see> to encrypt.</param>
    ''' <param name="protect">Type of <see cref="EncryptionStorage">key</see> to use to use to encrypt the data.</param>
    ''' <returns>Encrypted <see cref="String">value</see>.</returns>
    ''' <remarks></remarks>
    Public Function Encrypt(ByVal value As String, ByVal protect As EncryptionStorage) As String

      ' holds the result string
      Dim result As String = String.Empty


      ' blobs used in the CryptProtectData call
      Dim inBlob As New UnsafeNativeMethods.DATA_BLOB
      Dim entropyBlob As New UnsafeNativeMethods.DATA_BLOB
      Dim outBlob As New UnsafeNativeMethods.DATA_BLOB


      Try
        ' setup flags passed to the CryptProtectData call
        Dim flags As Integer = UnsafeNativeMethods.CRYPTPROTECT_UI_FORBIDDEN Or CInt(dotNetTips.Utility.General.IIf(protect = EncryptionStorage.Machine, UnsafeNativeMethods.CRYPTPROTECT_LOCAL_MACHINE, 0))

        ' setup input blobs, the data to be encrypted and entropy blob
        SetBlobData(inBlob, ASCIIEncoding.ASCII.GetBytes(value))
        SetBlobData(entropyBlob, _entropyData)

        ' call the DPAPI function, returns true if successful and fills in the outBlob
        If UnsafeNativeMethods.CryptProtectData(inBlob, String.Empty, entropyBlob, IntPtr.Zero, IntPtr.Zero, flags, outBlob) Then
          Dim resultBits As Byte() = GetBlobData(outBlob)
          If Not resultBits Is Nothing Then
            result = BytesToHex(resultBits) 'Convert.ToBase64String(resultBits)
          End If
        End If
      Finally
        ' clean up
        If inBlob.pbData.ToInt32() <> 0 Then
          Marshal.FreeHGlobal(inBlob.pbData)
        End If

        If entropyBlob.pbData.ToInt32() <> 0 Then
          Marshal.FreeHGlobal(entropyBlob.pbData)
        End If
      End Try

      Return result

    End Function

    ''' <summary>
    ''' Decrypts data using local machine key.
    ''' </summary>
    ''' <param name="value"><see cref="String">Value</see> to encrypt.</param>
    ''' <returns>Decrypted <see cref="String">value</see>.</returns>
    ''' <remarks></remarks>
    Public Function Decrypt(ByVal value As String) As String

      Return Me.Decrypt(value, EncryptionStorage.Machine)

    End Function

    ''' <summary>
    ''' Decrypts data.
    ''' </summary>
    ''' <param name="value"><see cref="String">Value</see> to encrypt.</param>
    ''' <param name="protect">Type of <see cref="EncryptionStorage">key</see> to use to use to decrypt the data.</param>
    ''' <returns>Decrypted <see cref="String">value</see>.</returns>
    ''' <remarks></remarks>
    Public Function Decrypt(ByVal value As String, ByVal protect As EncryptionStorage) As String
      ' holds the result string
      Dim result As String = String.Empty

      ' blobs used in the CryptUnprotectData call
      Dim inBlob As New UnsafeNativeMethods.DATA_BLOB
      Dim entropyBlob As New UnsafeNativeMethods.DATA_BLOB
      Dim outBlob As New UnsafeNativeMethods.DATA_BLOB

      Try
        ' setup flags passed to the CryptUnprotectData call
        Dim flags As Integer = UnsafeNativeMethods.CRYPTPROTECT_UI_FORBIDDEN Or CInt(dotNetTips.Utility.General.IIf(protect = EncryptionStorage.Machine, UnsafeNativeMethods.CRYPTPROTECT_LOCAL_MACHINE, 0))

        ' the CryptUnprotectData works with a byte array, convert string data
        Dim discarded As Integer
        Dim bits As Byte() = HexToBytes(value, discarded) ' Convert.FromBase64String(data)
        Debug.WriteLine(discarded.ToString(CultureInfo.CurrentCulture))

        ' setup input blobs, the data to be decrypted and entropy blob
        SetBlobData(inBlob, bits)
        SetBlobData(entropyBlob, _entropyData)

        ' call the DPAPI function, returns true if successful and fills in the outBlob
        If UnsafeNativeMethods.CryptUnprotectData(inBlob, Nothing, entropyBlob, IntPtr.Zero, IntPtr.Zero, flags, outBlob) Then
          Dim resultBits As Byte() = GetBlobData(outBlob)
          If Not resultBits Is Nothing Then
            result = ASCIIEncoding.ASCII.GetString(resultBits)
          End If
        End If
      Finally
        ' clean up
        If inBlob.pbData.ToInt32() <> 0 Then
          Marshal.FreeHGlobal(inBlob.pbData)
        End If

        If entropyBlob.pbData.ToInt32() <> 0 Then
          Marshal.FreeHGlobal(entropyBlob.pbData)
        End If
      End Try

      Return result
    End Function

    Private Sub SetEntropy()
      _entropyData = ASCIIEncoding.ASCII.GetBytes(System.Reflection.Assembly.GetExecutingAssembly.GetName.Name)
    End Sub
    ' internal methods

    ' helper method that fills in a  DATA_BLOB, copies 
    ' data from managed to unmanaged memory
    Private Sub SetBlobData(ByRef blob As UnsafeNativeMethods.DATA_BLOB, ByVal bits As Byte())
      blob.cbData = bits.Length
      blob.pbData = Marshal.AllocHGlobal(bits.Length)
      Marshal.Copy(bits, 0, blob.pbData, bits.Length)
    End Sub

    ' helper method that gets data from a DATA_BLOB, 
    ' copies data from unmanaged memory to managed
    Private Function GetBlobData(ByRef blob As UnsafeNativeMethods.DATA_BLOB) As Byte()
      ' return an empty string if the blob is empty
      If blob.pbData.ToInt32() = 0 Then Return Nothing

      ' copy information from the blob
      Dim data(blob.cbData - 1) As Byte
      Marshal.Copy(blob.pbData, data, 0, blob.cbData)
      UnsafeNativeMethods.LocalFree(blob.pbData)

      Return data

    End Function

    Private Function HexToBytes(ByVal hexString As String, ByRef discarded As Integer) As Byte()
      discarded = 0
      Dim newString As StringBuilder = New StringBuilder(hexString.Length)
      Dim c As Char
      Dim i As Integer = 0
      While i < hexString.Length
        c = hexString.Chars(i)
        If IsHexDigit(c) Then
          newString.Append(c)
        Else
          System.Math.Min(System.Threading.Interlocked.Increment(discarded), discarded - 1)
        End If
        System.Math.Min(System.Threading.Interlocked.Increment(i), i - 1)
      End While
      If Not (newString.Length Mod 2 = 0) Then
        System.Math.Min(System.Threading.Interlocked.Increment(discarded), discarded - 1)
        newString = newString.Remove(newString.Length - 1, 1)
      End If
      Dim byteLength As Integer = CInt(newString.Length / 2)
      Dim bytes(byteLength) As Byte
      i = 0
      Dim j As Integer = 0
      While i < byteLength
        Dim hex As String = New String(New Char() {newString(j), newString(j + 1)})
        bytes(i) = HexToByte(hex)
        System.Math.Min(System.Threading.Interlocked.Increment(i), i - 1)
        j += 2
      End While
      Return bytes
    End Function

    Private Function BytesToHex(ByVal bytes As Byte()) As String
      Dim hexString As StringBuilder = New StringBuilder(bytes.Length)
      Dim i As Integer = 0
      While i < bytes.Length
        hexString.Append(bytes(i).ToString("X2", CultureInfo.CurrentCulture))
        System.Math.Min(System.Threading.Interlocked.Increment(i), i - 1)
      End While
      Return hexString.ToString
    End Function

    'Private Function InHexFormat(ByVal hexString As String) As Boolean
    '  For Each digit As Char In hexString
    '    If Not IsHexDigit(digit) Then
    '      Return False
    '    End If
    '  Next
    '  Return True
    'End Function

    Private Function IsHexDigit(ByVal c As Char) As Boolean
      Dim numA As Integer = Convert.ToInt32("A"c)
      Dim num1 As Integer = Convert.ToInt32("0"c)
      c = Char.ToUpper(c, CultureInfo.CurrentCulture)
      Dim numChar As Integer = Convert.ToInt32(c)
      If numChar >= numA AndAlso numChar < (numA + 6) OrElse numChar >= num1 AndAlso numChar < (num1 + 10) Then
        Return True
      End If
      Return False
    End Function

    Private Function HexToByte(ByVal hex As String) As Byte
      If hex.Length > 2 OrElse hex.Length <= 0 Then
        Throw New ArgumentException("value must be 1 or 2 characters in length")
      End If
      Return Byte.Parse(hex, System.Globalization.NumberStyles.HexNumber, CultureInfo.CurrentCulture)
    End Function

  End Class

End Namespace